package com.yearner.reviewplan.user.data;

import com.yearner.reviewplan.common.enums.Consts;
import com.yearner.reviewplan.common.enums.RedisEnum;
import com.yearner.reviewplan.common.model.Token;
import com.yearner.reviewplan.common.util.*;
import com.yearner.reviewplan.user.UserRepresentation;
import com.yearner.reviewplan.user.exceptions.AuthIdFailedException;
import com.yearner.reviewplan.user.exceptions.UserNotFoundException;
import com.yearner.reviewplan.user.model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 用户验证
 *
 * @author pyg
 */
@Component
public class Identification implements InitializingBean {
    private final static Logger logger = LoggerFactory.getLogger(Identification.class);
    private static Identification sIdentification = null;
    private final RedisUtil redisUtil;
    private final UserRepresentation representation;
    private final IdentificationSession identificationSession;
    // token--id   id--token双重验证，防止横向越权
    /**
     * token--id
     */
//    private LinkedHashMap<String,String> mToken2Id;
//    /** id--token表   */
//    private LinkedHashMap<String,String> mId2Token;
//    private LinkedHashMap<String,User> mId2Object;
    private Queue<Token> mDelToken;
    private Queue<Token> mSaveToken;
    private final AsyncWorker asyncWorker;
    private final Executor asyncTask;

    public Identification(RedisUtil redisUtil, UserRepresentation representation, AsyncWorker asyncWorker, @Qualifier(value = "myTasks") Executor asyncTask, IdentificationSession identificationSession) {
        this.redisUtil = redisUtil;
        this.representation = representation;
        this.mDelToken = new LinkedBlockingQueue<>(6666);
        this.mSaveToken = new LinkedBlockingQueue<>(6666);
        this.asyncWorker = asyncWorker;
        this.asyncTask = asyncTask;
        this.identificationSession = identificationSession;
    }

    /**
     * 从redis取user
     *
     * @param userId id
     * @return
     */
    public User getUserById(String userId) {
        if (StringUtils.isBlank(userId)) {
            return null;
        }
        // 获取token,从Redis token---value
        String token = getToken(userId);
        // 1,token为null
        if (StringUtils.isBlank(token)) {
            return getUserFromRepository(userId);
        }
        // 2，token不为null
        // token 已存在，取值，如果为null中就冲数据库中取
        User user = redisUtil.getParseObjectFromMap(token, RedisEnum.token.USER_ACCOUNT, User.class);
        if (user == null) {
            user = getUserFromRepository(userId);
        }
        redisUtil.refreshExpireTime(token);
        return user;
    }

    public User getUserByToken(String token) {
        // token 已存在，取值，如果为null中就冲数据库中去
        User user = redisUtil.getParseObjectFromMap(token, RedisEnum.token.USER_ACCOUNT, User.class);
        if (user == null) {
            user = getUserFromRepository(getUserId(token));
        }
        redisUtil.refreshExpireTime(token);
        return user;
    }

    /**
     * 校验前端传递的id是否与用户相匹配
     *
     * @param argId   参数
     * @param request HttpServletRequest
     * @return false：argId为空；     true：匹配
     */
    public Boolean authUserId(String argId, HttpServletRequest request) {
        if (StringUtils.isBlank(argId)) {
            return false;
        }
        String trueUserId = request.getAttribute(Consts.CURRENT_USER_ID).toString();
        if (!trueUserId.equals(argId)) {
            identificationSession.addFailedCount(request);
            throw new AuthIdFailedException();
        }
        return true;
    }

    /**
     * 将user保存到 redis
     *
     * @param user
     */
    private void putUser(User user, String token) {
        // MAP_ID_2_TOKEN -> id--token
        redisUtil.pushMap(RedisEnum.MAP_KEY_ID_2_TOKEN, user.getUserId().toString(), token);
        // MAP_TOKEN_2_ID -> token -- id
        redisUtil.pushMap(RedisEnum.MAP_KEY_TOKEN_2_ID, token, user.getUserId().toString());
        // token -> key-obj 会过期
        redisUtil.pushMapExpire(token, RedisEnum.token.USER_ACCOUNT, user);
        redisUtil.refreshExpireTime(token);
    }

    /**
     * 从数据库中获取user数据，并放入Redis
     *
     * @param userId
     * @return
     */
    private User getUserFromRepository(String userId) {
        if (StringUtils.isBlank(userId)) {
            return null;
        }
        // 从数据库中获取
        User user = representation.getById(userId);
        // 将其放入redis
        String token = Optional.ofNullable(getToken(userId))
                .orElseGet(() -> newToken(user));
        return user;
    }

    public String newToken(User user) {
        String token = UuidGenerator.newUuid();
        putUser(user, token);
        mSaveToken.add(Token.of(user.getUserId().toString(), token));
        asyncWorker.addTask(this::saveToken);
        return token;
    }

    /**
     * 修改token
     *
     * @param user user
     * @return token
     */
    public String changeToken(User user, HttpSession session) {
        deleteToken(user);
        identificationSession.resetFailedCount();
        asyncWorker.addTask(this::deleteInMySql);
        return newToken(user);
    }

    /**
     * 删除token
     * @param user user
     */
    private void deleteToken(User user) {
        String oldToken = redisUtil.getParseObjectFromMap(RedisEnum.MAP_KEY_ID_2_TOKEN, user.getUserId().toString(), String.class);
        // 删除Map token -> type-obj
        redisUtil.delete(oldToken);
        // MAP_TOKEN_2_ID -> token 删除旧token
        redisUtil.hDelete(RedisEnum.MAP_KEY_TOKEN_2_ID, oldToken);
        mDelToken.add(Token.of(user.getUserId().toString(), oldToken));
    }


    public String getToken(String userId) {
        return redisUtil.getStringFromMap(RedisEnum.MAP_KEY_ID_2_TOKEN, userId);
    }

    private String getUserId(String token) {
        String userId = redisUtil.getStringFromMap(RedisEnum.MAP_KEY_TOKEN_2_ID, token);
        if (StringUtils.isBlank(userId)) {
            throw new UserNotFoundException();
        }
        return userId;
    }



    /*
     * ------  读取、保存到数据库  ------------
     */
    /**
     * 用户id -- token 表
     */
    private static final String TABLE_USER_2_TOKEN = "user_token";
    private static final String ID_COLUMN = "user_id";
    private static final String TOKEN_COLUMN = "token";

    /**
     * 初次加载时，读取数据库中保存的token
     */
    public void onLoadData() {
        logger.info("加载token数据......" + Thread.currentThread().getName());
        JdbcTemplate jdbcTemplate = new JdbcTemplate(MyJdbcUtil.sGetDataSource());
        String sql = "select * from user_token";
        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
        for (Map<String, Object> map : maps) {
            String userId = map.get(ID_COLUMN).toString();
            String token = map.get(TOKEN_COLUMN).toString();
            // MAP_ID_2_TOKEN -> id--token
            redisUtil.pushMap(RedisEnum.MAP_KEY_ID_2_TOKEN, userId, token);
            // MAP_TOKEN_2_ID -> token -- id
            redisUtil.pushMap(RedisEnum.MAP_KEY_TOKEN_2_ID, token, userId);
        }
    }

    public void deleteInMySql() {
        logger.info(Thread.currentThread().getName() + "......删除token数据");
        String sql = "delete from user_token where user_id= ? and token= ?";
        try {
            sqlAction(mDelToken, sql);
        } catch (SQLException e) {
            logger.error("在数据库删除oldToken失败", e);
        }
    }

    /**
     * 将token保存到数据库中
     */
    public void saveToken() {
        logger.info("......保存token数据");
        try {
            String sql = "insert into user_token values(?,?)";
            sqlAction(mSaveToken, sql);
            logger.info("......保存token成功！");
        } catch (SQLException e) {
            logger.error("数据库写入出错！ ", e);
        }
    }

    private void sqlAction(Queue<Token> queue, String sql) throws SQLException {
        Connection connection = MyJdbcUtil.getConnection();
        connection.setAutoCommit(false);
        for (Token value : queue) {
            PreparedStatement statement = connection.prepareStatement(sql);
            statement.setString(1, value.getUserId());
            statement.setString(2, value.getToken());
            statement.executeUpdate();
        }
        connection.commit();
        connection.setAutoCommit(true);
    }

    @Override
    public void afterPropertiesSet() {
        asyncTask.execute(this::onLoadData);
    }
}
